import {Inject, Injectable, Injector, OnDestroy, Optional} from '@angular/core';
import {
    ActivatedRoute,
    ActivatedRouteSnapshot,
    ExtraOptions,
    NavigationEnd,
    NavigationStart,
    Router,
    ROUTER_CONFIGURATION
} from '@angular/router';
import {BehaviorSubject, Observable, timer, Unsubscribable} from 'rxjs';

import {Menu, MenuService} from '@delon/theme';
import {ScrollService} from '@delon/util/browser';
import type {NzSafeAny} from 'ng-zorro-antd/core/types';

import {
    ReuseComponentRef,
    ReuseHookOnReuseInitType,
    ReuseHookTypes,
    ReuseTabCached,
    ReuseTabMatchMode,
    ReuseTabNotify,
    ReuseTabRouteParamMatchMode,
    ReuseTitle
} from './reuse-tab.interfaces';
import {ReuseTabStorageState, REUSE_TAB_STORAGE_KEY, REUSE_TAB_STORAGE_STATE} from './reuse-tab.state';

@Injectable({providedIn: 'root'})
export class ReuseTabService implements OnDestroy {
    private _inited = false;
    private _max = 10;
    private _keepingScroll = false;
    private _cachedChange = new BehaviorSubject<ReuseTabNotify | null>(null);
    private _cached: ReuseTabCached[] = [];
    private _titleCached: { [url: string]: ReuseTitle } = {};
    private _closableCached: { [url: string]: boolean } = {};
    private _router$?: Unsubscribable;
    private removeUrlBuffer: string | null = null;
    private positionBuffer: { [url: string]: [number, number] } = {};
    componentRef?: ReuseComponentRef;
    debug = false;
    routeParamMatchMode: ReuseTabRouteParamMatchMode = 'strict';
    mode = ReuseTabMatchMode.Menu;
    /** 排除规则，限 `mode=URL` */
    excludes: RegExp[] = [];
    storageState = false;

    private get snapshot(): ActivatedRouteSnapshot {
        return this.injector.get(ActivatedRoute).snapshot;
    }

    // #region public

    /**
     * Get init status
     *
     * 是否已经初始化完成
     */
    get inited(): boolean {
        return this._inited;
    }

    /**
     * Current routing address
     *
     * 当前路由地址
     */
    get curUrl(): string {
        return this.getUrl(this.snapshot);
    }

    /**
     * 允许最多复用多少个页面，取值范围 `2-100`，值发生变更时会强制关闭且忽略可关闭条件
     */
    set max(value: number) {
        this._max = Math.min(Math.max(value, 2), 100);
        for (let i = this._cached.length; i > this._max; i--) {
            this._cached.pop();
        }
    }

    set keepingScroll(value: boolean) {
        this._keepingScroll = value;
        this.initScroll();
    }

    get keepingScroll(): boolean {
        return this._keepingScroll;
    }

    keepingScrollContainer?: Element;

    /** 获取已缓存的路由 */
    get items(): ReuseTabCached[] {
        return this._cached;
    }

    /** 获取当前缓存的路由总数 */
    get count(): number {
        return this._cached.length;
    }

    /** 订阅缓存变更通知 */
    get change(): Observable<ReuseTabNotify | null> {
        return this._cachedChange.asObservable(); // .pipe(filter(w => w !== null));
    }

    /** 自定义当前标题 */
    set title(value: string | ReuseTitle) {
        const url = this.curUrl;
        if (typeof value === 'string') value = {text: value};
        this._titleCached[url] = value;
        this.di('update current tag title: ', value);
        this._cachedChange.next({
            active: 'title',
            url,
            title: value,
            list: this._cached
        });
    }

    /** 获取指定路径缓存所在位置，`-1` 表示无缓存 */
    index(url: string): number {
        return this._cached.findIndex(w => w.url === url);
    }

    /** 获取指定路径缓存是否存在 */
    exists(url: string): boolean {
        return this.index(url) !== -1;
    }

    /** 获取指定路径缓存 */
    get(url?: string): ReuseTabCached | null {
        return url ? this._cached.find(w => w.url === url) || null : null;
    }

    private remove(url: string | number, includeNonCloseable: boolean): boolean {
        const idx = typeof url === 'string' ? this.index(url) : url;
        const item = idx !== -1 ? this._cached[idx] : null;
        if (!item || (!includeNonCloseable && !item.closable)) return false;

        this.destroy(item._handle);

        this._cached.splice(idx, 1);
        delete this._titleCached[url];
        return true;
    }

    /**
     * 根据URL移除标签
     *
     * @param [includeNonCloseable=false] 是否强制包含不可关闭
     */
    close(url: string, includeNonCloseable: boolean = false): boolean {
        this.removeUrlBuffer = url;

        this.remove(url, includeNonCloseable);

        this._cachedChange.next({active: 'close', url, list: this._cached});

        this.di('close tag', url);
        return true;
    }

    /**
     * 清除右边
     *
     * @param [includeNonCloseable=false] 是否强制包含不可关闭
     */
    closeRight(url: string, includeNonCloseable: boolean = false): boolean {
        const start = this.index(url);
        for (let i = this.count - 1; i > start; i--) {
            this.remove(i, includeNonCloseable);
        }

        this.removeUrlBuffer = null;

        this._cachedChange.next({active: 'closeRight', url, list: this._cached});

        this.di('close right tages', url);
        return true;
    }

    /**
     * 清除所有缓存
     *
     * @param [includeNonCloseable=false] 是否强制包含不可关闭
     */
    clear(includeNonCloseable: boolean = false): void {
        this._cached.forEach(w => {
            if (!includeNonCloseable && w.closable) this.destroy(w._handle);
        });
        this._cached = this._cached.filter(w => !includeNonCloseable && !w.closable);

        this.removeUrlBuffer = null;

        this._cachedChange.next({active: 'clear', list: this._cached});

        this.di('clear all catch');
    }

    /**
     * 移动缓存数据
     *
     * @param url 要移动的URL地址
     * @param position 新位置，下标从 `0` 开始
     *
     * @example
     * ```
     * // source
     * [ '/a/1', '/a/2', '/a/3', '/a/4', '/a/5' ]
     * move('/a/1', 2);
     * // output
     * [ '/a/2', '/a/3', '/a/1', '/a/4', '/a/5' ]
     * move('/a/1', -1);
     * // output
     * [ '/a/2', '/a/3', '/a/4', '/a/5', '/a/1' ]
     * ```
     */
    move(url: string, position: number): void {
        const start = this._cached.findIndex(w => w.url === url);
        if (start === -1) return;
        const data = this._cached.slice();
        data.splice(position < 0 ? data.length + position : position, 0, data.splice(start, 1)[0]);
        this._cached = data;
        this._cachedChange.next({
            active: 'move',
            url,
            position,
            list: this._cached
        });
    }

    /**
     * 强制关闭当前路由（包含不可关闭状态），并重新导航至 `newUrl` 路由
     */
    replace(newUrl: string): void {
        const url = this.curUrl;
        if (this.exists(url)) {
            this.close(url, true);
        } else {
            this.removeUrlBuffer = url;
        }
        this.injector.get<Router>(Router).navigateByUrl(newUrl);
    }

    /**
     * 获取标题，顺序如下：
     *
     * 1. 组件内使用 `ReuseTabService.title = 'new title'` 重新指定文本
     * 2. 路由配置中 data 属性中包含 titleI18n > title
     * 3. 菜单数据中 text 属性
     *
     * @param url 指定URL
     * @param route 指定路由快照
     */
    getTitle(url: string, route?: ActivatedRouteSnapshot): ReuseTitle {
        if (this._titleCached[url]) {
            return this._titleCached[url];
        }

        if (route && route.data && (route.data['titleI18n'] || route.data['title'])) {
            return {
                text: route.data['title'],
                i18n: route.data['titleI18n']
            } as ReuseTitle;
        }

        const menu = this.getMenu(url);
        return menu ? {text: menu.text, i18n: menu.i18n} : {text: url};
    }

    /**
     * 清除标题缓存
     */
    clearTitleCached(): void {
        this._titleCached = {};
    }

    /** 自定义当前 `closable` 状态 */
    set closable(value: boolean) {
        const url = this.curUrl;
        this._closableCached[url] = value;
        this.di('update current tag closable: ', value);
        this._cachedChange.next({
            active: 'closable',
            closable: value,
            list: this._cached
        });
    }

    /**
     * 获取 `closable` 状态，顺序如下：
     *
     * 1. 组件内使用 `ReuseTabService.closable = true` 重新指定 `closable` 状态
     * 2. 路由配置中 data 属性中包含 `reuseClosable`
     * 3. 菜单数据中 `reuseClosable` 属性
     *
     * @param url 指定URL
     * @param route 指定路由快照
     */
    getClosable(url: string, route?: ActivatedRouteSnapshot): boolean {
        if (typeof this._closableCached[url] !== 'undefined') return this._closableCached[url];

        if (route && route.data && typeof route.data['reuseClosable'] === 'boolean') return route.data['reuseClosable'];

        const menu = this.mode !== ReuseTabMatchMode.URL ? this.getMenu(url) : null;
        if (menu && typeof menu['reuseClosable'] === 'boolean') return menu['reuseClosable'];

        return true;
    }

    /**
     * 清空 `closable` 缓存
     */
    clearClosableCached(): void {
        this._closableCached = {};
    }

    getTruthRoute(route: ActivatedRouteSnapshot): ActivatedRouteSnapshot {
        let next = route;
        while (next.firstChild) next = next.firstChild;
        return next;
    }

    /**
     * 根据快照获取URL地址
     */
    getUrl(route: ActivatedRouteSnapshot): string {
        let next = this.getTruthRoute(route);
        const segments: string[] = [];
        while (next) {
            segments.push(next.url.join('/'));
            next = next.parent!;
        }
        const url = `/${segments
            .filter(i => i)
            .reverse()
            .join('/')}`;
        return url;
    }

    /**
     * 检查快照是否允许被复用
     */
    can(route: ActivatedRouteSnapshot): boolean {
        const url = this.getUrl(route);
        if (url === this.removeUrlBuffer) return false;

        if (route.data && typeof route.data['reuse'] === 'boolean') return route.data['reuse'];

        if (this.mode !== ReuseTabMatchMode.URL) {
            const menu = this.getMenu(url);
            if (!menu) return false;
            if (this.mode === ReuseTabMatchMode.Menu) {
                if (menu.reuse === false) return false;
            } else {
                if (!menu.reuse || menu.reuse !== true) return false;
            }
            return true;
        }
        return !this.isExclude(url);
    }

    isExclude(url: string): boolean {
        return this.excludes.findIndex(r => r.test(url)) !== -1;
    }

    /**
     * 刷新，触发一个 refresh 类型事件
     */
    refresh(data?: NzSafeAny): void {
        this._cachedChange.next({active: 'refresh', data});
    }

    // #endregion

    // #region privates

    private destroy(_handle: NzSafeAny): void {
        if (_handle && _handle.componentRef && _handle.componentRef.destroy) _handle.componentRef.destroy();
    }

    private di(...args: NzSafeAny[]): void {
        // if (typeof ngDevMode === 'undefined' || ngDevMode) {
        //     if (!this.debug) return;
        //     console.warn(...args);
        // }
    }

    // #endregion

    constructor(
        private injector: Injector,
        private menuService: MenuService,
        @Optional() @Inject(REUSE_TAB_STORAGE_KEY) private stateKey: string,
        @Optional() @Inject(REUSE_TAB_STORAGE_STATE) private stateSrv: ReuseTabStorageState
    ) {
    }

    init(): void {
        this.initScroll();
        this._inited = true;
        this.loadState();
    }

    private loadState(): void {
        if (!this.storageState) return;

        this._cached = this.stateSrv.get(this.stateKey).map(v => ({
            title: {text: v.title},
            url: v.url,
            position: v.position
        }));
        this._cachedChange.next({active: 'loadState'});
    }

    private getMenu(url: string): Menu | null | undefined {
        const menus = this.menuService.getPathByUrl(url);
        if (!menus || menus.length === 0) return null;
        return menus.pop();
    }

    runHook(
        method: ReuseHookTypes,
        comp: ReuseComponentRef | number | undefined,
        type: ReuseHookOnReuseInitType = 'init'
    ): void {
        if (typeof comp === 'number') {
            const item = this._cached[comp];
            comp = item._handle?.componentRef;
        }
        if (comp == null || !comp.instance) {
            return;
        }
        const compThis = comp.instance;
        const fn = compThis[method];
        if (typeof fn !== 'function') {
            return;
        }
        if (method === '_onReuseInit') {
            fn.call(compThis, type);
        } else {
            (fn as () => void).call(compThis);
        }
    }

    private hasInValidRoute(route: ActivatedRouteSnapshot): boolean {
        return !route.routeConfig || !!route.routeConfig.loadChildren || !!route.routeConfig.children;
    }

    /**
     * 决定是否允许路由复用，若 `true` 会触发 `store`
     */
    shouldDetach(route: ActivatedRouteSnapshot): boolean {
        if (this.hasInValidRoute(route)) return false;
        this.di('#shouldDetach', this.can(route), this.getUrl(route));
        return this.can(route);
    }

    /**
     * 存储
     */
    store(_snapshot: ActivatedRouteSnapshot, _handle: NzSafeAny): void {
        const url = this.getUrl(_snapshot);
        const idx = this.index(url);
        const isAdd = idx === -1;

        const item: ReuseTabCached = {
            title: this.getTitle(url, _snapshot),
            closable: this.getClosable(url, _snapshot),
            position: this.getKeepingScroll(url, _snapshot) ? this.positionBuffer[url] : null,
            url,
            _snapshot,
            _handle
        };
        if (isAdd) {
            if (this.count >= this._max) {
                // Get the oldest closable location
                const closeIdx = this._cached.findIndex(w => w.closable!);
                if (closeIdx !== -1) this.remove(closeIdx, false);
            }
            this._cached.push(item);
        } else {
            // Current handler is null when activate routes
            // For better reliability, we need to wait for the component to be attached before call _onReuseInit
            const cahcedComponentRef = this._cached[idx]._handle?.componentRef;
            if (_handle == null && cahcedComponentRef != null) {
                timer(100).subscribe(() => this.runHook('_onReuseInit', cahcedComponentRef));
            }
            this._cached[idx] = item;
        }
        this.removeUrlBuffer = null;

        this.di('#store', isAdd ? '[new]' : '[override]', url);

        if (_handle && _handle.componentRef) {
            this.runHook('_onReuseDestroy', _handle.componentRef);
        }

        if (!isAdd) {
            this._cachedChange.next({active: 'override', item, list: this._cached});
        }
    }

    /**
     * 决定是否允许应用缓存数据
     */
    shouldAttach(route: ActivatedRouteSnapshot): boolean {
        if (this.hasInValidRoute(route)) return false;
        const url = this.getUrl(route);
        const data = this.get(url);
        const ret = !!(data && data._handle);
        this.di('#shouldAttach', ret, url);
        if (!ret) {
            this._cachedChange.next({active: 'add', url, list: this._cached});
        }
        return ret;
    }

    /**
     * 提取复用数据
     */
    retrieve(route: ActivatedRouteSnapshot): NzSafeAny | null {
        if (this.hasInValidRoute(route)) return null;
        const url = this.getUrl(route);
        const data = this.get(url);
        const ret = (data && data._handle) || null;
        this.di('#retrieve', url, ret);
        return ret;
    }

    /**
     * 决定是否应该进行复用路由处理
     */
    shouldReuseRoute(future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot): boolean {
        let ret = future.routeConfig === curr.routeConfig;
        if (!ret) return false;

        const path = ((future.routeConfig && future.routeConfig.path) || '') as string;
        if (path.length > 0 && ~path.indexOf(':')) {
            if (this.routeParamMatchMode === 'strict') {
                ret = this.getUrl(future) === this.getUrl(curr);
            } else {
                ret = path === ((curr.routeConfig && curr.routeConfig.path) || '');
            }
        }
        this.di('=====================');
        this.di('#shouldReuseRoute', ret, `${this.getUrl(curr)}=>${this.getUrl(future)}`, future, curr);
        return ret;
    }

    // #region scroll

    /**
     * 获取 `keepingScroll` 状态，顺序如下：
     *
     * 1. 路由配置中 data 属性中包含 `keepingScroll`
     * 2. 菜单数据中 `keepingScroll` 属性
     * 3. 组件 `keepingScroll` 值
     */
    getKeepingScroll(url: string, route?: ActivatedRouteSnapshot): boolean {
        if (route && route.data && typeof route.data['keepingScroll'] === 'boolean') return route.data['keepingScroll'];

        const menu = this.mode !== ReuseTabMatchMode.URL ? this.getMenu(url) : null;
        if (menu && typeof menu['keepingScroll'] === 'boolean') return menu['keepingScroll'];

        return this.keepingScroll;
    }

    private get isDisabledInRouter(): boolean {
        const routerConfig = this.injector.get<ExtraOptions>(ROUTER_CONFIGURATION, {} as NzSafeAny);
        return routerConfig.scrollPositionRestoration === 'disabled';
    }

    private get ss(): ScrollService {
        return this.injector.get(ScrollService);
    }

    private initScroll(): void {
        if (this._router$) {
            this._router$.unsubscribe();
        }

        this._router$ = this.injector.get<Router>(Router).events.subscribe(e => {
            if (e instanceof NavigationStart) {
                const url = this.curUrl;
                if (this.getKeepingScroll(url, this.getTruthRoute(this.snapshot))) {
                    this.positionBuffer[url] = this.ss.getScrollPosition(this.keepingScrollContainer);
                } else {
                    delete this.positionBuffer[url];
                }
            } else if (e instanceof NavigationEnd) {
                const url = this.curUrl;
                const item = this.get(url);
                if (item && item.position && this.getKeepingScroll(url, this.getTruthRoute(this.snapshot))) {
                    if (this.isDisabledInRouter) {
                        this.ss.scrollToPosition(this.keepingScrollContainer, item.position);
                    } else {
                        setTimeout(() => this.ss.scrollToPosition(this.keepingScrollContainer, item.position!), 1);
                    }
                }
            }
        });
    }

    // #endregion

    ngOnDestroy(): void {
        const {_cachedChange, _router$} = this;
        this.clear();
        this._cached = [];
        _cachedChange.complete();

        if (_router$) {
            _router$.unsubscribe();
        }
    }
}
